{
 "cells": [
  {
   "cell_type": "code",
   "execution_count": 1,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "matplotlib.get_backend :  module://ipykernel.pylab.backend_inline\n"
     ]
    }
   ],
   "source": [
    "import os\n",
    "GPU_id = 0\n",
    "os.environ['CUDA_VISIBLE_DEVICES'] = str(GPU_id)\n",
    "    \n",
    "from mpnn_model.common import * \n",
    "from mpnn_model.common_constants import * \n",
    "from mpnn_model.dataset import TensorBatchDataset, BatchDataBunch, BatchDataLoader\n",
    "from mpnn_model.data_collate import tensor_collate_rnn\n",
    "from mpnn_model.GaussRank import GaussRankMap\n",
    "from mpnn_model.helpers import load_cfg\n",
    "from mpnn_model.model import Net \n",
    "from mpnn_model.train_loss import train_criterion, lmae_criterion\n",
    "from mpnn_model.callback import get_reverse_frame, lmae, LMAE\n",
    "from mpnn_model.radam import * \n",
    "from mpnn_model.build_predictions import do_test \n",
    "from mpnn_model.helpers import * \n",
    "\n",
    "# Fast ai\n",
    "from fastai.tabular import *\n",
    "from fastai.basic_data import DataBunch\n",
    "from fastai.basic_data import *\n",
    "from fastai.callbacks import SaveModelCallback\n",
    "from fastai import *\n",
    "\n",
    "\n",
    "import cudf as gd\n",
    "import numpy as np \n",
    "import pandas as pd \n",
    "\n",
    "from torch.utils.dlpack import from_dlpack\n",
    "import torch\n",
    "from torch import _utils\n",
    "from fastai.torch_core import to_device\n",
    "import torch.nn.functional as F \n",
    "\n",
    "from timeit import default_timer as timer\n",
    "from datetime import datetime\n",
    "from time import time \n",
    "from functools import partial\n",
    "import glob \n",
    "import warnings\n",
    "warnings.filterwarnings(\"ignore\") "
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 2,
   "metadata": {},
   "outputs": [],
   "source": [
    "from scripts.train_type import *\n",
    "from mpnn_model.common_constants import COUPLING_TYPE"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 3,
   "metadata": {},
   "outputs": [],
   "source": [
    "model_dict = { '1JHC': 'lmae', '2JHC': 'lmae', '3JHC': 'lmae', '3JHH': 'lmae',\n",
    "             '1JHN': 'mlmae' , '2JHN':'mlmae' , '3JHN':'mlmae', '2JHH':'mlmae'}"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 4,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "['1JHC', '2JHC', '3JHC', '1JHN', '2JHN', '3JHN', '2JHH', '3JHH']"
      ]
     },
     "execution_count": 4,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "COUPLING_TYPE"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 5,
   "metadata": {},
   "outputs": [],
   "source": [
    "NUM_TARGET =  1   "
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 6,
   "metadata": {},
   "outputs": [],
   "source": [
    "cfg ='/rapids/notebooks/srabhi/champs-2019/CherKeng_solution/fastai_code/experiments/MPNN_RNN_MAE_WO_GAUSSRANK_SINGLE_TYPE.yaml'\n",
    "fold = 1\n",
    "type_='3JHH'\n",
    "COUPLING_MAX = COUPLING_MAX_DICT[type_]"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 7,
   "metadata": {},
   "outputs": [],
   "source": [
    "cfg = load_cfg(cfg)\n",
    "DATA_DIR = cfg['dataset']['input_path']\n",
    "normalize = cfg['dataset']['normalize']\n",
    "gaussrank=  cfg['dataset']['gaussrank']\n",
    "model_name = cfg['train']['model_name']  \n",
    "model_name = model_name+ '_fold_%s' %fold \n",
    "batch_size = cfg['train']['batch_size']\n",
    "predict_type = cfg['train']['predict_type']\n",
    "loss_name = cfg['train']['loss_name']\n",
    "predict_type = cfg['model']['regression']['predict_type']\n",
    "epochs = cfg['train']['epochs']\n",
    "max_lr = cfg['train']['max_lr']\n",
    "device = cfg['train']['device']"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "<h1> Dataset </h1>"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 8,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "\n",
      " Load Train/Validation features for fold 1\n",
      "\n",
      " Get In-memory Tensor \n",
      "CPU times: user 30.6 s, sys: 13.1 s, total: 43.7 s\n",
      "Wall time: 1min 35s\n"
     ]
    }
   ],
   "source": [
    "%%time\n",
    "test= pd.read_csv(DATA_DIR+'/csv/test.csv')\n",
    "id_test = test.id.values\n",
    "mol_test = test.molecule_name.values\n",
    "\n",
    "print('\\n Load Train/Validation features for fold %s' %fold)\n",
    "validation = gd.read_parquet(DATA_DIR +'/rnn_parquet/fold_%s/%s/validation.parquet'%(fold, type_))\n",
    "train = gd.read_parquet(DATA_DIR +'/rnn_parquet/fold_%s/%s/train.parquet' %(fold, type_))\n",
    "\n",
    "print('\\n Get In-memory Tensor ')\n",
    "\n",
    "# Convert train to tensors \n",
    "num_nodes_tensor = from_dlpack(train['num_nodes'].to_dlpack()).long()\n",
    "num_edges_tensor = from_dlpack(train['num_edge'].to_dlpack()).long()\n",
    "num_coupling_tensor = from_dlpack(train['num_coupling'].to_dlpack()).long()\n",
    "\n",
    "node_cols = [i for i in train.columns if re.compile(\"^node_[0-9]+\").findall(i)]\n",
    "nodes_matrix = from_dlpack(train[node_cols].to_dlpack()).type(torch.float32)\n",
    "\n",
    "edge_cols = [i for i in train.columns if re.compile(\"^edge_[0-9]+\").findall(i)]\n",
    "edges_matrix = from_dlpack(train[edge_cols].to_dlpack()).type(torch.float32)\n",
    "\n",
    "coupling_cols = [i for i in train.columns if re.compile(\"^coupling_[0-9]+\").findall(i)]\n",
    "coupling_matrix = from_dlpack(train[coupling_cols].to_dlpack()).type(torch.float32)\n",
    "\n",
    "mol_train = train.molecule_name.unique().to_pandas().values\n",
    "train_dataset = TensorBatchDataset(mol_train, \n",
    "                                   tensors=[nodes_matrix, edges_matrix, coupling_matrix,\n",
    "                                            num_nodes_tensor, num_edges_tensor, num_coupling_tensor], \n",
    "                                    batch_size=batch_size,\n",
    "                                   collate_fn=tensor_collate_rnn,\n",
    "                                   COUPLING_MAX=COUPLING_MAX,\n",
    "                                    mode='train',\n",
    "                                    csv='train')\n",
    "# convert validation to tensors \n",
    "num_nodes_tensor = from_dlpack(validation['num_nodes'].to_dlpack()).long()\n",
    "num_edges_tensor = from_dlpack(validation['num_edge'].to_dlpack()).long()\n",
    "num_coupling_tensor = from_dlpack(validation['num_coupling'].to_dlpack()).long()\n",
    "\n",
    "node_cols = [i for i in validation.columns if re.compile(\"^node_[0-9]+\").findall(i)]\n",
    "nodes_matrix = from_dlpack(validation[node_cols].to_dlpack()).type(torch.float32)\n",
    "\n",
    "edge_cols = [i for i in validation.columns if re.compile(\"^edge_[0-9]+\").findall(i)]\n",
    "edges_matrix = from_dlpack(validation[edge_cols].to_dlpack()).type(torch.float32)\n",
    "\n",
    "coupling_cols = [i for i in validation.columns if re.compile(\"^coupling_[0-9]+\").findall(i)]\n",
    "coupling_matrix = from_dlpack(validation[coupling_cols].to_dlpack()).type(torch.float32)\n",
    "\n",
    "\n",
    "mol_valid = validation.molecule_name.unique().to_pandas().values\n",
    "valid_dataset = TensorBatchDataset(mol_valid, \n",
    "                                   tensors=[nodes_matrix, edges_matrix, coupling_matrix,\n",
    "                                            num_nodes_tensor, num_edges_tensor, num_coupling_tensor], \n",
    "                                    batch_size=batch_size,\n",
    "                                   collate_fn=tensor_collate_rnn,\n",
    "                                   COUPLING_MAX=COUPLING_MAX,\n",
    "                                    mode='train',\n",
    "                                    csv='train')\n",
    "\n",
    "del train \n",
    "del validation \n",
    "\n",
    "data = BatchDataBunch.create(train_dataset, valid_dataset, device=device, bs=batch_size)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Model "
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 9,
   "metadata": {},
   "outputs": [],
   "source": [
    "pretrain_model = model_dict[type_]\n",
    "freeze_cycle = 1\n",
    "unfreeze_cycle = 1 "
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 11,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/html": [
       "<table border=\"1\" class=\"dataframe\">\n",
       "  <thead>\n",
       "    <tr style=\"text-align: left;\">\n",
       "      <th>epoch</th>\n",
       "      <th>train_loss</th>\n",
       "      <th>valid_loss</th>\n",
       "      <th>LMAE</th>\n",
       "      <th>time</th>\n",
       "    </tr>\n",
       "  </thead>\n",
       "  <tbody>\n",
       "    <tr>\n",
       "      <td>0</td>\n",
       "      <td>0.783770</td>\n",
       "      <td>0.739121</td>\n",
       "      <td>0.760839</td>\n",
       "      <td>01:27</td>\n",
       "    </tr>\n",
       "  </tbody>\n",
       "</table>"
      ],
      "text/plain": [
       "<IPython.core.display.HTML object>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Better model found at epoch 0 with LMAE value: 0.7608386278152466.\n"
     ]
    },
    {
     "data": {
      "text/html": [
       "<table border=\"1\" class=\"dataframe\">\n",
       "  <thead>\n",
       "    <tr style=\"text-align: left;\">\n",
       "      <th>epoch</th>\n",
       "      <th>train_loss</th>\n",
       "      <th>valid_loss</th>\n",
       "      <th>LMAE</th>\n",
       "      <th>time</th>\n",
       "    </tr>\n",
       "  </thead>\n",
       "  <tbody>\n",
       "    <tr>\n",
       "      <td>0</td>\n",
       "      <td>-0.165126</td>\n",
       "      <td>-0.247940</td>\n",
       "      <td>-0.225796</td>\n",
       "      <td>02:40</td>\n",
       "    </tr>\n",
       "  </tbody>\n",
       "</table>"
      ],
      "text/plain": [
       "<IPython.core.display.HTML object>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Better model found at epoch 0 with LMAE value: -0.22579647600650787.\n"
     ]
    }
   ],
   "source": [
    "if not gaussrank: \n",
    "    net = torch.load('pre_trained_models/coupling_%s_%s_fold_%s_wo_gaussrank.pth'%(type_, pretrain_model, fold))\n",
    "else: \n",
    "    net = torch.load('pre_trained_models/coupling_%s_%s_fold_%s_gaussrank.pth'%(type_, pretrain_model, fold))\n",
    "        \n",
    "# load grm : \n",
    "data_dir = DATA_DIR + '/rnn_parquet'\n",
    "file = glob.glob(data_dir+'/fold_%s/'%fold+'%s/*.csv'%type_)[0]     \n",
    "coupling_order = [type_]\n",
    "mapping_frames = [pd.read_csv(file)]  \n",
    "grm = GaussRankMap(mapping_frames, coupling_order)\n",
    "\n",
    "\n",
    "############################------------- Fine tune training ---------------################################\n",
    "optal = partial(RAdam)\n",
    "learn =  Learner(data,\n",
    "                 net,\n",
    "                 metrics=None,\n",
    "                 opt_func=optal,\n",
    "                 callback_fns=partial(LMAE,\n",
    "                                    grm=grm,\n",
    "                                    predict_type=predict_type,\n",
    "                                    normalize_coupling=normalize, \n",
    "                                    coupling_rank=gaussrank))\n",
    "\n",
    "learn.loss_func = lmae_criterion\n",
    "\n",
    "learn.split([[learn.model.preprocess,learn.model.message_function, learn.model.update_function, learn.model.readout],\n",
    "             [learn.model.rnn_attention],[learn.model.dense_layer, learn.model.predict]])\n",
    "\n",
    "learn.lr_range(slice(1e-3))\n",
    "\n",
    "learn.freeze()\n",
    "learn.fit_one_cycle(freeze_cycle, callbacks=[SaveModelCallback(learn,\n",
    "                                                 every='improvement',\n",
    "                                                 monitor='LMAE', \n",
    "                                                 name=cfg['train']['model_name']+'_fold_%s_frozen_type_%s_'%(fold, type_),\n",
    "                                                 mode='min')])\n",
    "\n",
    "learn.unfreeze()\n",
    "learn.fit_one_cycle(unfreeze_cycle, max_lr=max_lr, callbacks=[SaveModelCallback(learn,\n",
    "                                                 every='improvement',\n",
    "                                                 monitor='LMAE', \n",
    "                                                 name=cfg['train']['model_name']+'_fold_%s_pretrained_%s_'%(fold, type_),\n",
    "                                                 mode='min')])"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "<h1> Predictions </h1>"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 30,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "\n",
      " Compute predictions for validation data at fold 1\n",
      "\n",
      "   147689/       1     147689.00   0 hr 00 min\n",
      "\n",
      "predict\n",
      "build preds frame\n",
      "Compute lmae per type\n",
      "\n",
      "Validation loss is : -0.2508597426602501\n",
      "\n",
      "Save model to disk\n",
      "load test data\n",
      "\n",
      " Compute predictions for test data at fold 1\n",
      "\n",
      "   147689/       1     147689.00   0 hr 00 min\n",
      "\n",
      "predict\n",
      "build preds frame\n",
      "Compute lmae per type\n"
     ]
    }
   ],
   "source": [
    "valid_dataset = TensorBatchDataset(mol_valid, \n",
    "                                tensors=[nodes_matrix, edges_matrix, coupling_matrix,\n",
    "                                        num_nodes_tensor, num_edges_tensor, num_coupling_tensor], \n",
    "                                batch_size=batch_size,\n",
    "                                collate_fn=tensor_collate_rnn,\n",
    "                                COUPLING_MAX=COUPLING_MAX,\n",
    "                                mode='test',\n",
    "                                csv='train')\n",
    "\n",
    "valid_loader = BatchDataLoader(valid_dataset, \n",
    "                               shuffle=False, \n",
    "                               pin_memory=False, \n",
    "                               drop_last=False, \n",
    "                               device='cuda')\n",
    "\n",
    "print('\\n Compute predictions for validation data at fold %s\\n' %fold)  \n",
    "valid_loss, reverse_frame, contributions, molecule_representation = do_test(learn.model,\n",
    "                                                                       valid_loader,\n",
    "                                                                       1,\n",
    "                                                                       1,\n",
    "                                                                       predict_type,\n",
    "                                                                       grm,\n",
    "                                                                       normalize=normalize,\n",
    "                                                                       gaussrank=gaussrank)\n",
    "\n",
    "\n",
    "val_loss = valid_loss[-3]\n",
    "print('\\nValidation loss is : %s' %val_loss)\n",
    "\n",
    "print('\\nSave model to disk')\n",
    "torch.save(learn.model, 'models/' + cfg['train']['model_name'] + '_fold_%s_final_save.pth'%fold)\n",
    "\n",
    "print('load test data')\n",
    "torch.cuda.empty_cache()\n",
    "test = gd.read_parquet(DATA_DIR +'/rnn_parquet/test_%s.parquet'%type_)\n",
    "num_nodes_tensor = from_dlpack(test['num_nodes'].to_dlpack())\n",
    "num_edges_tensor = from_dlpack(test['num_edge'].to_dlpack())\n",
    "num_coupling_tensor = from_dlpack(test['num_coupling'].to_dlpack())\n",
    "node_cols = [i for i in test.columns if re.compile(\"^node_[0-9]+\").findall(i)]\n",
    "nodes_matrix = from_dlpack(test[node_cols].to_dlpack())\n",
    "nodes_matrix = from_dlpack(test[node_cols].to_dlpack()).type(torch.float32)\n",
    "edge_cols = [i for i in test.columns if re.compile(\"^edge_[0-9]+\").findall(i)]\n",
    "edges_matrix = from_dlpack(test[edge_cols].to_dlpack()).type(torch.float32)\n",
    "coupling_cols = [i for i in test.columns if re.compile(\"^coupling_[0-9]+\").findall(i)]\n",
    "coupling_matrix = from_dlpack(test[coupling_cols].to_dlpack()).type(torch.float32)\n",
    "\n",
    "mol_test  = test.molecule_name.unique().to_pandas().values\n",
    "#batch_node, batch_edge, batch_coupling, batch_graussrank, batch_num_node, batch_num_edge, batch_num_coupling\n",
    "del test\n",
    "\n",
    "test_dataset = TensorBatchDataset(mol_test, \n",
    "                                tensors=[nodes_matrix, edges_matrix, coupling_matrix,\n",
    "                                         num_nodes_tensor, num_edges_tensor, num_coupling_tensor], \n",
    "                                batch_size=batch_size,\n",
    "                                collate_fn=tensor_collate_rnn,\n",
    "                                COUPLING_MAX=COUPLING_MAX,\n",
    "                                mode='test',\n",
    "                                csv='test')\n",
    "\n",
    "test_loader = BatchDataLoader(test_dataset, \n",
    "                               shuffle=False, \n",
    "                               pin_memory=False, \n",
    "                               drop_last=False, \n",
    "                               device='cuda')\n",
    "\n",
    "print('\\n Compute predictions for test data at fold %s\\n' %fold)\n",
    "test_loss, preds_fold_test, contributions, molecule_representation = do_test(learn.model,\n",
    "                                                                       valid_loader,\n",
    "                                                                       1,\n",
    "                                                                       1,\n",
    "                                                                       predict_type,\n",
    "                                                                       grm,\n",
    "                                                                       normalize=normalize,\n",
    "                                                                       gaussrank=gaussrank)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 31,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/html": [
       "<div>\n",
       "<style scoped>\n",
       "    .dataframe tbody tr th:only-of-type {\n",
       "        vertical-align: middle;\n",
       "    }\n",
       "\n",
       "    .dataframe tbody tr th {\n",
       "        vertical-align: top;\n",
       "    }\n",
       "\n",
       "    .dataframe thead th {\n",
       "        text-align: right;\n",
       "    }\n",
       "</style>\n",
       "<table border=\"1\" class=\"dataframe\">\n",
       "  <thead>\n",
       "    <tr style=\"text-align: right;\">\n",
       "      <th></th>\n",
       "      <th>scalar_coupling_constant</th>\n",
       "      <th>type_ind</th>\n",
       "      <th>id</th>\n",
       "      <th>true_scalar_coupling_constant</th>\n",
       "    </tr>\n",
       "  </thead>\n",
       "  <tbody>\n",
       "    <tr>\n",
       "      <th>0</th>\n",
       "      <td>11.331904</td>\n",
       "      <td>7</td>\n",
       "      <td>111.0</td>\n",
       "      <td>3.42929</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>1</th>\n",
       "      <td>8.175097</td>\n",
       "      <td>7</td>\n",
       "      <td>112.0</td>\n",
       "      <td>12.33070</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>2</th>\n",
       "      <td>9.798114</td>\n",
       "      <td>7</td>\n",
       "      <td>117.0</td>\n",
       "      <td>12.33180</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>3</th>\n",
       "      <td>4.748343</td>\n",
       "      <td>7</td>\n",
       "      <td>118.0</td>\n",
       "      <td>3.42409</td>\n",
       "    </tr>\n",
       "  </tbody>\n",
       "</table>\n",
       "</div>"
      ],
      "text/plain": [
       "   scalar_coupling_constant  type_ind     id  true_scalar_coupling_constant\n",
       "0                 11.331904         7  111.0                        3.42929\n",
       "1                  8.175097         7  112.0                       12.33070\n",
       "2                  9.798114         7  117.0                       12.33180\n",
       "3                  4.748343         7  118.0                        3.42409"
      ]
     },
     "execution_count": 31,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "reverse_frame.head(4)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Save predictions "
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 34,
   "metadata": {},
   "outputs": [],
   "source": [
    "OUT_DIR = cfg['dataset']['output_path']\n",
    "num_output = cfg['model']['regression']['num_output']\n",
    "if num_output == 1:\n",
    "    out_dir = OUT_DIR + '/submit/scalar_output/'\n",
    "    # init preditions arrays \n",
    "    pred_cv = np.zeros( cfg['train']['train_shape'])\n",
    "    pred_sub = np.zeros(cfg['train']['test_shape'])\n",
    "\n",
    "elif num_output == 5:\n",
    "    out_dir = OUT_DIR + '/submit/multi_output/'\n",
    "    pred_cv = np.zeros((cfg['train']['train_shape'], 5))\n",
    "    pred_sub = np.zeros((cfg['train']['test_shape'], 5))"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 36,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "\n",
      " Save Validation frame\n",
      "\n",
      " Save Test frame\n"
     ]
    }
   ],
   "source": [
    "print('\\n Save Validation frame' )\n",
    "clock = \"{}\".format(datetime.now()).replace(' ','-').replace(':','-').split('.')[0]\n",
    "output_name = out_dir + '/cv_%s_%s_%.4f_type_%s_fold_%s.csv.gz'%(clock, pretrain_model, val_loss, type_, fold)\n",
    "reverse_frame.to_csv(output_name, index=False,compression='gzip')\n",
    "\n",
    "# save test predictions \n",
    "print('\\n Save Test frame' )\n",
    "clock = \"{}\".format(datetime.now()).replace(' ','-').replace(':','-').split('.')[0]\n",
    "output_name = out_dir + '/sub_%s_%s_%.4f_type_%s_fold_%s.csv.gz'%(clock, pretrain_model, val_loss, type_, fold)\n",
    "preds_fold_test.to_csv(output_name, index=False,compression='gzip')\n",
    "\n",
    "net=None\n",
    "torch.cuda.empty_cache()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": []
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.7.3"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 4
}
